Skip to content

feat(telegram): native DM draft streaming (4.30 — vercel/chat#340)#140

Merged
patrick-chinchill merged 1 commit into
mainfrom
integ/wt30-telegram
Jun 18, 2026
Merged

feat(telegram): native DM draft streaming (4.30 — vercel/chat#340)#140
patrick-chinchill merged 1 commit into
mainfrom
integ/wt30-telegram

Conversation

@patrick-chinchill

Copy link
Copy Markdown
Collaborator

0.4.30 wave. Cherry-picks 8f290d1 onto current main: native DM draft streaming via sendMessageDraft + the core stream()->RawMessage|None widening + None-handling in Thread._handle_stream. No version bump. Independent review needed.

vercel/chat#340)

Port of upstream chat@4.30.0's Telegram private-chat draft streaming
(changeset 5461ea9, PR vercel/chat#340).

Adapter (src/chat_sdk/adapters/telegram/adapter.py):
- Add `stream()`: DMs get native draft streaming via the `sendMessageDraft`
  Bot API method. The draft bubble updates in place as chunks arrive,
  throttled to `options.update_interval_ms`
  (default TELEGRAM_DEFAULT_STREAM_UPDATE_INTERVAL_MS = 250ms), then a
  regular `sendMessage` persists the final text. Returns `None` for non-DM
  threads BEFORE consuming any chunks so the SDK's built-in post+edit
  fallback handles groups, supergroups, and channels.
- Add `with_telegram_markdown_fallback()`: shared retry-without-`parse_mode`
  path now wraps `post_message`, `edit_message`, `send_document`, and
  `send_attachment`. When Telegram rejects MarkdownV2 with a
  "can't parse entities" 400, the call retries once with the plain-text
  rendering (or the original text when stripping markup left nothing).
- Add `render_plain_text_message`, `resolve_telegram_fallback_text`,
  `create_draft_id` (int32-wrapping, wall-clock-seeded), and
  `_create_telegram_document_form_data` (form bodies are single-use, so the
  retry rebuilds them).

Core (src/chat_sdk/thread.py, src/chat_sdk/types.py):
- `Adapter.stream` / `BaseAdapter.stream` may now return
  `RawMessage | None`. `None` (returned before any chunk is consumed)
  delegates to the post+edit fallback for the current thread.
- `Thread._handle_stream` checks `if raw_result is not None`; on `None` it
  closes the unused wrapper generator and falls through to the fallback.
  The existing `RawMessage.text` override (Teams cancellation reconciliation)
  is preserved inside the non-None branch.

Documented divergence (docs/UPSTREAM_SYNC.md): unlike upstream's
`thread.ts`, we do NOT seed `update_interval_ms` with the thread default.
Our hand-rolled Teams native streaming path treats any non-`None` value as a
caller override of its 1500ms quota-protecting throttle; seeding 500ms would
silently drop Teams DM throttling below the platform's ~1 req/sec quota.
Adapters apply their own default when the field is `None`, and
`_fallback_stream` already substitutes `self._streaming_update_interval_ms`,
so the fallback path stays observably identical to upstream.

Tests: all 12 upstream `it()` blocks ported to
tests/test_telegram_streaming.py plus the `stream returns null` fallback
test to tests/test_thread_faithful.py, with Python-only coverage for the
default-interval, whitespace-rejection, and empty-fallback-text branches.

Parity note: this is a partial 4.30.0 slice; UPSTREAM_PARITY stays at
4.29.0 (the rest of the 4.30 wave is tracked separately).

https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@patrick-chinchill, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 59 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 620b35cf-dc05-4548-8c92-956f88b0ac04

📥 Commits

Reviewing files that changed from the base of the PR and between 209601d and d797f2c.

📒 Files selected for processing (6)
  • docs/UPSTREAM_SYNC.md
  • src/chat_sdk/adapters/telegram/adapter.py
  • src/chat_sdk/thread.py
  • src/chat_sdk/types.py
  • tests/test_telegram_streaming.py
  • tests/test_thread_faithful.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements native DM draft streaming and markdown parse-error fallback for the Telegram adapter, porting features from upstream vercel/chat#340. Specifically, it introduces the stream method in TelegramAdapter to update draft bubbles in real-time for private chats using sendMessageDraft, while delegating non-DM threads back to the SDK's built-in post+edit fallback by returning None. It also adds a robust fallback mechanism (with_telegram_markdown_fallback) that retries message posts, edits, and attachments without a parse mode if Telegram rejects MarkdownV2 entities. Corresponding updates were made to Thread._handle_stream to handle None return values from adapter streams, and comprehensive tests were added to validate these behaviors. I have no feedback to provide as there are no review comments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d797f2c532

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1559 to +1560
if not self.is_dm(thread_id):
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delegate username chats to fallback streaming

When the thread id is a valid Telegram channel/supergroup username such as telegram:@announcements, is_dm() returns true because the chat id does not start with -, so this new gate does not delegate to the SDK fallback. sendMessageDraft only works for private numeric chat ids, so the first draft call is rejected, draft updates are disabled, and the stream is consumed until a single final message is posted instead of using the post+edit fallback for non-DMs. Gate native drafts on a positive numeric chat id.

Useful? React with 👍 / 👎.

Comment on lines +1686 to +1689
final_postable: AdapterPostableMessage = (
PostableMarkdown(markdown=accumulated)
if stream_uses_markdown
else self.resolve_telegram_fallback_text(accumulated, _markdown_to_plain_text(accumulated))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Finalize markdown before persisting DM streams

When a DM stream ends with markdown the renderer repaired for the draft preview, for example chunks ending in **bold, flush_draft() has just sent the repaired renderer.render() text, but the final sendMessage is built from raw accumulated. Telegram then persists literal/unrepaired markup while users just saw the repaired draft, and Thread.stream records the raw accumulator because the returned RawMessage.text is unset. Use renderer.finish() or otherwise return the text that was actually sent for the final markdown send.

Useful? React with 👍 / 👎.

@patrick-chinchill patrick-chinchill merged commit 7a2cd7f into main Jun 18, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants